09. Star Shower
L3 08 Shower
Star Shower
Now for the final step, you will create a slightly more involved animation, animating multiple properties on multiple objects.
For this effect, a button click will result in the creation of a star with a random size, which will be added to the background container, just out of view of the top of that container. The star will proceed to fall down to the bottom of the screen, accelerating as it goes. As it falls, it will rotate.
For this step, you will fill in the shower() function, to wire up a single animation of a falling star to a single click of the SHOWER button. There are a few new concepts here, in addition to things you’ve seen in the previous steps.
Step 1: A Star is Born
First, you’re going to need some local variables to hold state that we will need in the ensuing code. Specifically, you’ll need:
- a reference to the star field
ViewGroup(which is just the parent of the current star view). - the width and height of that container (which you will use to calculate the end translation values for our falling stars).
- the default width and height of our star (which you will later alter with a scale factor to get different-sized stars).
- Start filling out the inside of the
shower()function. Add this code:
val container = star.parent as ViewGroup
val containerW = container.width
val containerH = container.height
var starW: Float = star.width.toFloat()
var starH: Float = star.height.toFloat()
- Create a new
Viewto hold the star graphic. Because the star is aVectorDrawableasset, use anAppCompatImageView, which has the ability to host that kind of resource. Create the star and add it to the background container.
val newStar = AppCompatImageView(this)
newStar.setImageResource(R.drawable.ic_star)
newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT)
container.addView(newStar)
- Run the app. Click on the SHOWER button. You will see the new star you created in the top-left corner.
Step 2: Sizing and positioning the star
You haven’t yet told this image where to be positioned in the container, so it’s positioned at (0, 0) by default. You will fix this placement in this step.
- Set the size of the star. Modify the star to have a random size, from .1x to 1.6x of its default size. Use this scale factor to change the cached width/height values, because you will need to know the actual pixel height/width for later calculations.
newStar.scaleX = Math.random().toFloat() * 1.5f + .1f
newStar.scaleY = newStar.scaleX
starW *= newStar.scaleX
starH *= newStar.scaleY
You have now cached the star’s pixel H/W stored in starW and starH:
- Now position the new star. Horizontally, it should appear randomly somewhere from the left edge to the right edge. This code uses the width of the star to position it from half-way off the screen on the left (
-starW / 2) to half-way off the screen on the right (with the star positioned at (containerW - starW / 2). The vertical positioning of the star will be handled later in the actual animation code.
newStar.translationX = Math.random().toFloat() *
containerW - starW / 2
Step 3: Creating animators for star rotation and falling
You’re done setting up the initial star information; it’s time to work on the animation. The star should rotate as it falls downwards. You’ve already seen one way to animate two properties together, using PropertyValuesHolder, in the previous task on scaling. You could do a similar thing here, except there will be different types of motion, what we call “interpolation,” on these two animations. Specifically, the rotation will use a smooth linear motion (moving at a constant rate over the entire rotation animation), while the falling animation will use an accelerating motion (simulating gravity pulling the star downward at a constantly faster rate). So you'll create two animators and add an interpolator to each..
- First, create two animators, along with their interpolators:
val mover = ObjectAnimator.ofFloat(newStar, View.TRANSLATION_Y,
-starH, containerH + starH)
mover.interpolator = AccelerateInterpolator(1f)
val rotator = ObjectAnimator.ofFloat(newStar, View.ROTATION,
(Math.random() * 1080).toFloat())
rotator.interpolator = LinearInterpolator()
The mover animation is responsible for making the star “fall.” It animates the TRANSLATION_Y property, similar to what you did with TRANSLATION_X in the earlier translation task, but causing vertical instead of horizontal motion. The code animates from -starH to (containerH + starH), which effectively places it just off the container at the top and moves it until it’s just outside the container at the bottom, as shown here:
The AccelerateInterpolator “interpolator” that we are setting on the star causes a gentle acceleration motion.
For the rotation animation, the star will rotate a random amount between 0 and 1080 degrees (three times around). For the motion, we are simply using a LinearInterpolator, so the rotation will proceed at a constant rate as the star falls.
Note: There are several interpolators in the Android system, some more powerful and flexible than others, such as PathInterpolator. You can experiment with them to see which ones you like for your UI animations.
Step 4: Running the animations in parallel with AnimatorSet
Now it is time to put these two animators together into a single AnimatorSet, which is useful for this slightly more complex animation involving multiple ObjectAnimators. AnimatorSet is basically a group of animations, along with instructions on when to run those animations. It can play animations in parallel, as you will do here, or sequentially (like you might do in the list-fading example mentioned earlier, where you first fade out a view and then animate the resulting gap closed). An AnimatorSet can also contain other AnimatorSets, so you can create very complex hierarchical choreography by grouping animators together into these sets.
- Create the
AnimatorSetand add the child animators to it (along with information to play them in parallel). The default animation time of 300 milliseconds is too quick to enjoy the falling stars, so set the duration to a random number between 500 and 2000 milliseconds, so stars fall at different speeds.
val set = AnimatorSet()
set.playTogether(mover, rotator)
set.duration = (Math.random() * 1500 + 500).toLong()
- Once
newStarhas fallen off the bottom of the screen, it should be removed from the container. Set a simple listener to wait for the end of the animation and remove it. Then start the animation.
Remember: It should seem obvious that when a view is not needed anymore, it should be removed. But complex animations can make us forget about the simple things. Use animation listeners to handle important tasks for bringing views onto or off of the screen, like we’re doing here, to remove a view when the animation to move it off the screen is complete.
set.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
container.removeView(newStar)
}
})
set.start()
- Run your application. You can click on the SHOWER button multiple times, creating a new star and new animation each time. Note that you didn’t have to disable the button during the animation, as you did in the earlier tasks, because this time we wanted to create several simultaneous animations. There was no problem with discontinuous motion artifacts because each animation is independent of the others and operates on a different target object.
You should see something like this:
Congratulations
Congratulations, you've successfully built an app that runs several different kinds of property animations. Animating stars may not be the kind of UI experience you want in your applications, but the tools you used in this lab are exactly the tools you should use to animate UI elements in real-world situations. ObjectAnimator, AnimatorSet, LinearInterpolator, PropertyValuesHolder; these are all good APIs to understand in order to write animations in your code.